import chips.jsonlib.types as abc
__all__ = ('value, container, json, value_ext, container_ext, json_ext'.split(', '))

def _get_conversion(tt, cache, bad_cache, json_types):
    '''
    Finds the appropriate conversor function for given type 'tt'.
    
    
    Arguments
    ---------
    tt : type
        Type that shall be converted.
        
    cache : dict
        Mapping between python types and the corresponding JSON type.
        
    json_types : set
        Allowed JSON types for conversion function. 
    '''
    # Select which function will perform the conversion to JSON value 
    try:
        # Investigate if any subclass is recognized
        for tt_ in tt.mro():
            if tt_ in cache:
                json_type = cache[tt_]
                cache[tt] = json_type
                break
            elif tt_ in bad_cache:
                raise TypeError('unsupported data type, %s' % repr(tt))

        # If no subclass of tt is recognized, json_type will not be defined
        # and trying to use it will raise a UnboundLocalError.
        conv = JSON_TYPE_CONVERSIONS[json_type]
    except UnboundLocalError:
        json_type = None
        for C, conv in JSON_TYPE_CONVERSIONS.items():
            if issubclass(tt, C):
                if C in json_types:
                    json_type = cache[tt] = C
                break
        if json_type is None:
            bad_cache.append(tt)
            raise TypeError('unsupported data type, %s' % repr(tt))

    # Execute the conversion
    return conv

def value(obj, encoding=None):
    '''
    Convert 'obj' to the canonical JSON type. i.e., strs are converted to 
    unicodes, numerical types become ints or floats, etc.
         

    Arguments
    ---------
    
    obj : Value
        Object that accepts conversion. Otherwise, a TypeError exception is 
        raised.
        
    encoding : str
        Encoding used to convert string types to unicode (not necessary for
        ASCII strings).
    
    Examples
    --------
   
    # Normalize strings
    >>> value('some string')
    u'some string'
    
    # Normalize numerical types
    >>> import decimal, fractions
    >>> d, f = value(decimal.Decimal('0.5')), value(fractions.Fraction(1,2))
    >>> d, type(d), f, type(f)
    (0.5, <type 'float'>, 0.5, <type 'float'>)
    
    # Raise TypeError for non-Value types
    >>> value((1,2,3))
    Traceback (most recent call last):
    ...
    TypeError: unsupported data type, <type 'tuple'>
    '''

    return _get_conversion(type(obj), VALUE_CONVERTERS, VALUE_BAD, VALUE_TYPES)(obj)

def container(obj, encoding=None):
    '''
    Convert container 'obj' to the canonical JSON type. i.e., tuples and other
    containers are converted to lists and mappings are converted to dicts. 
         

    Arguments
    ---------
    
    obj : Value
        Object that accepts conversion. Otherwise, a TypeError exception is 
        raised.
        
    encoding : str
        Encoding used to convert substrings in the container structure. 
    
    Examples
    --------

    # Lists become tuples, and subitems are also converted
    >>> container((42, 'answer'))
    [42, u'answer']
    >>> container({'foo': 'bar', 'ham': ('spam', 'eggs')})
    {u'foo': u'bar', u'ham': [u'spam', u'eggs']}
    
    # Differently from python, JSON dicts *do not* accept non-string keys.
    >>> container({1:1, 2:4, 3:9})
    Traceback (most recent call last):
    ...
    TypeError: only strings are allowed as keys, got <type 'int'>
    '''

    return _get_conversion(type(obj), CONTAINER_CONVERTERS, CONTAINER_BAD, CONTAINER_TYPES)(obj, encoding=encoding)

def json(obj, encoding=None):
    '''
    Convert 'obj' to the canonical JSON type. 
         

    Arguments
    ---------
    
    obj : Value
        Object that accepts conversion. Otherwise, a TypeError exception is 
        raised.
        
    encoding : str
        Encoding used to convert substrings in the container structure. 
    
    Examples
    --------

    # Lists become tuples, and subitems are also converted
    >>> json((42, 'answer'))
    [42, u'answer']
    >>> json({'foo': 'bar', 'ham': ('spam', 'eggs')})
    {u'foo': u'bar', u'ham': [u'spam', u'eggs']}
    '''

    conv = _get_conversion(type(obj), JSON_CONVERTERS, JSON_BAD, JSON_TYPES)
    if conv is listconv or conv is dictconv:
        return conv(obj, encoding=encoding)
    elif conv is JSON_TYPE_CONVERSIONS[abc.Str]:
        return conv(obj)
    else:
        return conv(obj)

#===============================================================================
#                     Specialized container conversions 
#===============================================================================
def listconv(obj, json_conv=json, encoding=None):
    '''
    Convert container-like objects to JSON-Arrays (implemented as python lists).
    '''
    return [ json_conv(item, encoding) for item in obj ]

def dictconv(obj, json_conv=json, encoding=None):
    '''
    Convert mapping-like objects to JSON-Objects (implemented as python dicts).
    '''
    dic = {}
    for k, v in obj.iteritems():
        if not isinstance(k, abc.Str):
            raise TypeError('only strings are allowed as keys, got %s' % type(k))
        dic[unicode(k)] = json_conv(v, encoding)
    return dic

#===============================================================================
#                            Extended JSON objects
#===============================================================================
def value_ext(obj):
    return _conversion_worker(obj, VALUEEXT_CONVERTERS, VALUEEXT_TYPES)

def container_ext(obj):
    return _conversion_worker(obj, CONTAINEREXT_CONVERTERS, CONTAINEREXT_TYPES)

def json_ext(obj):
    return _conversion_worker(obj, JSONEXT_CONVERTERS, JSONEXT_TYPES)

#===============================================================================
#                         GLOBAL STATE DICTIONARIES
#===============================================================================

# Define the dict of canonical conversions from JSON to python objects.
CANONICAL_TYPES = {
   'object': dictconv,
   'array': listconv,
   'string': unicode,
   'int': int,
   'real': float,
   'bool': bool,
   'null': type(None) }

CANONICAL_TYPECHECKS = {
   'object': abc.Object,
   'array': abc.Array,
   'string': abc.Str,
   'int': abc.Int,
   'real': abc.Real,
   'bool': abc.Bool,
   'null': abc.Null }

# This dict will be used to perform conversions to the canonical types and can 
# be modified temporarily by context managers. This is hability is useful
# if the user wants to convert e.g., reals to Decimals, arrays to tuples, etc.  
JSON_TYPE_CONVERSIONS = dict((CANONICAL_TYPECHECKS[k], v)
                              for k, v in CANONICAL_TYPES.items())


# Function types: specify the types which function is allowed to convert 
VALUE_TYPES = set([ abc.Str, abc.Null, abc.Bool, abc.Int, abc.Real ])
VALUEEXT_TYPES = set([ abc.Str, abc.Null, abc.Bool, abc.Int, abc.Real ])
CONTAINER_TYPES = set([ abc.Array, abc.Object ])
CONTAINEREXT_TYPES = set([ abc.Array, abc.Object ])
JSON_TYPES = VALUE_TYPES.union(CONTAINER_TYPES)
JSONEXT_TYPES = VALUEEXT_TYPES.union(CONTAINEREXT_TYPES)

# Function converters: maps types to conversions in specific functions.
VALUE_CONVERTERS = {}
VALUEEXT_CONVERTERS = {}
CONTAINER_CONVERTERS = {}
CONTAINEREXT_CONVERTERS = {}
JSON_CONVERTERS = {}
JSONEXT_CONVERTERS = {}

# Bad cache: types that are known to not being accepted by a given conversor
VALUE_BAD = set()
VALUEEXT_BAD = set()
CONTAINER_BAD = set()
CONTAINEREXT_BAD = set()
JSON_BAD = set()
JSONEXT_BAD = set()

if __name__ == '__main__':
    import doctest
    doctest.testmod(optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)

